<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      * {
        margin: 0;
        padding: 0;
      }
      #cvs {
        position: fixed;
        top: 0;
        left: 0;
      }
    </style>
  </head>
  <body>
    <canvas id="cvs"></canvas>

    <!-- 由三元一次不等式计算出矩阵的系数 x y z -->
    <script>
      function elimination(eq1, eq2) {
        // 消元时乘以的系数
        const e = eq1[1] / eq2[1];
        // 保存消元后得到的新已知量的数组
        const ret = [];
        for (let i = 0; i < eq1.length; ++i) {
          if (i !== 1) {
            ret.push(eq1[i] - eq2[i] * e);
          }
        }
        return ret;
      }
      function gaussian_elimination([eq1, eq2, eq3]) {
        const [a1, b1, c1, d1] = eq1;

        const _eq1 = elimination(eq1, eq2);
        const _eq2 = elimination(eq2, eq3);
        const [a2, c2, d2] = _eq2;

        const [a3, d3] = elimination(_eq1, _eq2);

        const x = d3 / a3;
        const z = (d2 - a2 * x) / c2;
        const y = (d1 - a1 * x - c1 * z) / b1;

        return [x, y, z];
      }
    </script>

    <!-- 贝赛尔曲线 -->
    <script>
      // 根据一阶贝塞尔曲线，计算出对应曲线上点的坐标
      function getPointFromBezier(t, p0, p1) {
        const a = 1 - t;
        const b = t;

        return {
          x: a * p0.x + b * p1.x,
          y: a * p0.y + b * p1.y,
        };
      }

      // 根据三阶贝塞尔曲线，计算出对应曲线上点的坐标
      function getPointFromCubicBezier(t, p0, p1, p2, p3) {
        const a = 1 - t;
        const b = t;

        const t0 = a * a * a;
        const t1 = 3 * a * a * b;
        const t2 = 3 * a * b * b;
        const t3 = b * b * b;

        return {
          x: t0 * p0.x + t1 * p1.x + t2 * p2.x + t3 * p3.x,
          y: t0 * p0.y + t1 * p1.y + t2 * p2.y + t3 * p3.y,
        };
      }
    </script>

    <!-- 分割四边形 根据三点计算出对应的矩阵系数 a b c d e f -->
    <script>
      function getMatrix(after, before) {
        // 分离单元
        let {
          0: a,
          1: c,
          2: e,
        } = gaussian_elimination([
          [before[0].x, before[0].y, 1, after[0].x],
          [before[1].x, before[1].y, 1, after[1].x],
          [before[2].x, before[2].y, 1, after[2].x],
        ]);
        let {
          0: b,
          1: d,
          2: f,
        } = gaussian_elimination([
          [before[0].x, before[0].y, 1, after[0].y],
          [before[1].x, before[1].y, 1, after[1].y],
          [before[2].x, before[2].y, 1, after[2].y],
        ]);

        return {
          a,
          b,
          c,
          d,
          e,
          f,
        };
      }

      function splitRect(rect, iRect, count = 9) {
        let pos = [];
        const [a, b, c, d] = rect;
        const [ia, ib, ic, id] = iRect;
        for (let i = 0; i < count + 1; ++i) {
          const row = [];
          const it = i / count;
          const dc1 = getPointFromBezier(1 / 3, ia, id);
          const dc2 = getPointFromBezier(2 / 3, ia, id);
          const bc1 = getPointFromBezier(1 / 3, ib, ic);
          const bc2 = getPointFromBezier(2 / 3, ib, ic);
          const i_a = getPointFromBezier(it, ia, id);
          const i_b = getPointFromBezier(it, ib, ic);

          const p0 = getPointFromCubicBezier(it, a, dc1, dc2, d);
          const p1 = getPointFromBezier(1 / 3, i_a, i_b);
          const p2 = getPointFromBezier(2 / 3, i_a, i_b);
          const p3 = getPointFromCubicBezier(it, b, bc1, bc2, c);

          for (let j = 0; j < count + 1; ++j) {
            const t = j / count;
            row.push(getPointFromCubicBezier(t, p0, p1, p2, p3));
          }
          pos.push(row);
        }
        return pos;
      }
    </script>

    <!-- 渲染形状 -->
    <script>
      function renderShape(
        ctx,
        points,
        cb = (ctx) => {
          ctx.fillStyle = "red";
          ctx.fill();
        }
      ) {
        ctx.save();
        ctx.beginPath();
        for (let i = 0; i < points.length; i++) {
          const point = points[i];
          if (i === 0) ctx.moveTo(point.x, point.y);
          else ctx.lineTo(point.x, point.y);
        }
        ctx.closePath();
        cb(ctx);
        ctx.restore();
      }

      function renderStorke(ctx, points, iPoints) {
        ctx.save();
        const len = points.length - 1;
        for (let i = 0; i < 4; ++i) {
          //#region 绘制横向线条
          const r_p0 = points[Math.floor((len * i) / 3)][0];
          const r_p1 = iPoints[Math.floor((len * i) / 3)][Math.floor(len / 3)];
          const r_p2 = iPoints[Math.floor((len * i) / 3)][Math.floor((len * 2) / 3)];
          const r_p3 = points[Math.floor((len * i) / 3)][len];
          ctx.beginPath();
          ctx.moveTo(r_p0.x, r_p0.y);
          ctx.bezierCurveTo(r_p1.x, r_p1.y, r_p2.x, r_p2.y, r_p3.x, r_p3.y);
          ctx.stroke();
          //#endregion

          //#region 绘制纵向线条
          const c_p0 = points[0][Math.floor((len * i) / 3)];
          const c_p1 = iPoints[Math.floor(len / 3)][Math.floor((len * i) / 3)];
          const c_p2 = iPoints[Math.floor((len * 2) / 3)][Math.floor((len * i) / 3)];
          const c_p3 = points[len][Math.floor((len * i) / 3)];
          ctx.beginPath();
          ctx.moveTo(c_p0.x, c_p0.y);
          ctx.bezierCurveTo(c_p1.x, c_p1.y, c_p2.x, c_p2.y, c_p3.x, c_p3.y);
          ctx.stroke();
          //#endregion
        }

        ctx.restore();
      }

      function renderClipImage(ctx, img, path, iPath, vector, origin, isBottom = false) {
        renderShape(ctx, path, (ctx) => {
          ctx.clip();
          const { a, b, c, d, e, f } = getMatrix(path, iPath);
          ctx.transform(a, b, c, d, e, f);
          ctx.drawImage(
            img,
            vector.x - origin.x,
            vector.y - origin.y,
            img.width + 2,
            img.height + 2,
            vector.x,
            vector.y,
            img.width + 2,
            img.height + 2
          );
        });
      }
    </script>

    <!-- scheduler -->
    <script>
      let id = 1;
      class Task {
        id = id++;
        sortIndex;
        startTime;
        expireTime;

        constructor(fn, delay = 0) {
          this.fn = fn;
          let now = performance.now();
          this.sortIndex = now;
          this.startTime = now;
          this.expireTime = now + delay;
        }

        run() {
          return this.fn();
        }
      }

      class MinHeap {
        constructor(compare) {
          this.heap = [];
          this.compare = compare;
        }

        get size() {
          return this.heap.length;
        }

        push(value) {
          let last = this.size;
          this.heap.push(value);
          this.shiftUp(last);
        }

        pop() {
          if (this.size === 0) return null;
          const first = this.heap[0];
          const last = this.heap.pop();
          if (last !== first) {
            this.heap[0] = last;
            this.shiftDown(0);
          }
          return first;
        }

        peek() {
          return this.size === 0 ? null : this.heap[0];
        }

        shiftUp(i) {
          while (i > 0) {
            const p = i >>> 1;
            if (this.compare(this.heap[i], this.heap[p])) {
              [this.heap[i], this.heap[p]] = [this.heap[p], this.heap[i]];
            }
            i = p;
          }
        }

        shiftDown(i) {
          let half = this.size >>> 1;
          let node = this.heap[i];
          while (i < half) {
            let left = (i << 1) + 1;
            let right = left + 1;
            if (this.compare(this.heap[left], node)) {
              if (right < this.size && !this.compare(this.heap[right], this.heap[left])) {
                [node, this.heap[right]] = [this.heap[right], node];
                i = right;
              } else {
                [node, this.heap[left]] = [this.heap[left], node];
                i = left;
              }
            } else if (right < this.size && !this.compare(this.heap[right], node)) {
              [node, this.heap[right]] = [this.heap[right], node];
              i = right;
            } else {
              return;
            }
          }
        }
      }

      const minHeap = new MinHeap((a, b) => {
        if (a.sortIndex < b.sortIndex) return true;
        return a.sortIndex === b.sortIndex && a.id < b.id;
      });

      let running = false;
      function scheduler(fn, delay = 0) {
        minHeap.push(new Task(fn, delay));
        runTask();
      }
      function runTask() {
        if (running) return;
        running = true;
        requestAnimationFrame(handleRunTask);
      }
      function handleRunTask(timestamp) {
        while (performance.now() - timestamp < 15) {
          if (minHeap.size === 0) {
            running = false;
            return;
          }
          let task = minHeap.peek();
          let now = performance.now();
          if (now < task.expireTime) {
            task.sortIndex = now;
            minHeap.shiftDown(0);
            continue;
          }
          task.run();
          minHeap.pop();
        }
        running = false;
        if (minHeap.size === 0) return;
        runTask();
      }
    </script>

    <script>
      const cvs = document.getElementById("cvs");
      cvs.width = window.innerWidth;
      cvs.height = window.innerHeight;
      const ctx = cvs.getContext("2d");
      let img = new Image();
      img.src = "https://picsum.photos/400/400";
      img.onload = () => {
        const pos = { x: 600, y: 200 };
        const w = img.width;
        const h = img.height;
        const imgPos = [pos, { x: pos.x + w, y: pos.y }, { x: pos.x + w, y: pos.y + h }, { x: pos.x, y: pos.y + h }];
        const iImgPos = imgPos.map((p) => ({ ...p }));

        const iPoints = splitRect(imgPos, iImgPos, 9);
        function renderFromPoints(ctx, points) {
          for (let i = 0; i < points.length - 1; i++) {
            for (let j = 0; j < points[i].length - 1; j++) {
              let iTarget = {
                x: iPoints[i][j].x - 1,
                y: iPoints[i][j].y - 1,
              };
              let iStart = {
                x: iPoints[0][0].x - 1,
                y: iPoints[0][0].y - 1,
              };
              let topPath = [
                { x: points[i][j].x - 1, y: points[i][j].y - 1 },
                { x: points[i][j + 1].x + 2, y: points[i][j + 1].y - 1 },
                { x: points[i + 1][j].x - 1, y: points[i + 1][j].y + 2 },
              ];
              let iTopPath = [
                { x: iPoints[i][j].x - 1, y: iPoints[i][j].y - 1 },
                { x: iPoints[i][j + 1].x + 2, y: iPoints[i][j + 1].y - 1 },
                { x: iPoints[i + 1][j].x - 1, y: iPoints[i + 1][j].y + 2 },
              ];
              renderClipImage(ctx, img, topPath, iTopPath, iTarget, iStart);
              let bottomPath = [
                { x: points[i + 1][j + 1].x, y: points[i + 1][j + 1].y },
                { x: points[i][j + 1].x, y: points[i][j + 1].y - 1 },
                { x: points[i + 1][j].x - 1, y: points[i + 1][j].y },
              ];
              let iBottomPath = [
                { x: iPoints[i + 1][j + 1].x, y: iPoints[i + 1][j + 1].y },
                { x: iPoints[i][j + 1].x, y: iPoints[i][j + 1].y - 1 },
                { x: iPoints[i + 1][j].x - 1, y: iPoints[i + 1][j].y },
              ];
              renderClipImage(ctx, img, bottomPath, iBottomPath, iTarget, iStart, true);
            }
          }
        }
        // renderFromPoints(ctx, iPoints);

        scheduler(function render(time) {
          imgPos[0].x = Math.max(0, imgPos[0].x - 1);
          imgPos[0].y = Math.max(0, imgPos[0].y - 1);

          imgPos[1].x = Math.min(cvs.width, imgPos[1].x + 1);
          imgPos[1].y = Math.max(0, imgPos[1].y - 1);

          imgPos[2].x = Math.min(cvs.width, imgPos[2].x + 1);
          imgPos[2].y = Math.min(cvs.height, imgPos[2].y + 1);

          imgPos[3].x = Math.max(0, imgPos[3].x - 1);
          imgPos[3].y = Math.min(cvs.height, imgPos[3].y + 1);

          const points = splitRect(imgPos, iImgPos, 9);
          ctx.clearRect(0, 0, cvs.width, cvs.height);
          renderFromPoints(ctx, points);
          renderStorke(ctx, points, iPoints);
          scheduler(render, 10);
        }, 10);
      };
    </script>
  </body>
</html>
